package com.krolik22.maven.repgen.report;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.shared.dependency.tree.DependencyNode;
import org.apache.maven.shared.dependency.tree.filter.DependencyNodeFilter;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
import org.codehaus.plexus.util.cli.DefaultConsumer;

import java.io.File;
import java.io.PrintStream;
import java.util.*;

/**
 * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
 */
public class DependencyVisualizer {

    LinkedHashMap<String, Node> nodes = new LinkedHashMap<String, Node>();
    LinkedHashSet<Edge> edges = new LinkedHashSet<Edge>();
    LinkedHashMap<String, Cluster> clusters = new LinkedHashMap<String, Cluster>();
    List<String> scopes = new ArrayList<String>();
    DependencyNodeFilter filter;
    boolean hideOptional = true;
    boolean hideOmitted = true;
    boolean hideVersion = true;
    boolean hideGroupId = true;
    boolean hideType = true;
    boolean hideTransitive;
    boolean keepDot;
    List<ClusterGraph> clustersMap;
    String label;
    Log log;
    boolean cascade;
    String direction = "TB";

    private class Cluster {
        private final String id;
        private final ArrayList<Node> children = new ArrayList<Node>();
        private final String name;
        private final String color;

        private Cluster(String id, String name, String color) {
            this.id = id;
            this.name = name;
            this.color = color;
        }

        public ArrayList<Node> getChildren() {
            return children;
        }

        @Override
        public boolean equals(Object obj) {
            return id.equals(((Node) obj).id);
        }

        @Override
        public int hashCode() {
            return id.hashCode();
        }

        @Override
        public String toString() {
            return id;
        }

        public String getId() {
            return id;
        }

        public String getLabel() {
            return name;
        }

        public String getColor() {
            return color;
        }
    }

    private class Node {
        private final String id;
        private final ArrayList<Edge> children = new ArrayList<Edge>();
        private final ArrayList<Edge> parents = new ArrayList<Edge>();
        private final Artifact artifact;
        private int roots;

        public Node(String id, Artifact artifact) {
            this.id = id;
            this.artifact = artifact;
        }

        @Override
        public boolean equals(Object obj) {
            return id.equals(((Node) obj).id);
        }

        @Override
        public int hashCode() {
            return id.hashCode();
        }

        @Override
        public String toString() {
            return id;
        }

        public String getId() {
            return id;
        }


        public String getLabel() {
            final Artifact a = artifact;
            StringBuilder sb = new StringBuilder();
            if (!hideGroupId) {
                sb.append(a.getGroupId() + "\\n");
            }
            sb.append(a.getArtifactId());
            if (!hideType) {
                if (!isExclusivelyType("jar")) {
                    sb.append("\\n");
                    boolean first = true;
                    for (String type : getTypes()) {
                        if (!first) {
                            sb.append(" | ");
                        }
                        first = false;
                        sb.append(type);
                    }
                }
            }
            if (!hideVersion) {
                sb.append("\\n" + a.getBaseVersion());
            }
            return sb.toString();
        }

        public String getColor() {
            if (isScope("test")) {
                return "#0c7500";
            }
            if (isOptional()) {
                return "cornflowerblue";
            }
            if (isScope("provided")) {
                return "darkgrey";
            }
            return "black";
        }

        private boolean isScope(String scope) {
            return roots == 0 && !parents.isEmpty() && allMatchScope(parents, scope);
        }

        public String getFillColor() {
            if (roots > 0) {
                return "#dddddd";
            }
            return "white";
        }

        public String getFontColor() {
            return getColor();
        }

        public String getLineStyle() {
            String rc = isOptional() ? "dotted" : "solid";
            rc += ",filled";
            return rc;
        }

        public double getFontSize() {
            if (roots > 0) {
                return 14;
            }
            return 8;
        }

        public boolean isOptional() {
            return roots == 0 && !parents.isEmpty() && allMatchOptional(parents, true);
        }


        private boolean allMatchScope(ArrayList<Edge> edges, String scope) {
            for (Edge e : edges) {
                if (!e.isScope(scope)) {
                    return false;
                }
            }
            return true;
        }

        private boolean allMatchOptional(ArrayList<Edge> edges, boolean value) {
            for (Edge e : edges) {
                if (e.optional != value) {
                    return false;
                }
            }
            return true;
        }

        private Set<String> getTypes() {
            LinkedHashSet<String> rc = new LinkedHashSet<String>();
            rc.add(artifact.getType()
//                    + (artifact.getClassifier() == null ? "" : (":" + artifact.getClassifier()))
            );
            for (Edge e : parents) {
                Artifact artifact = e.dependencyNode.getArtifact();
                rc.add(artifact.getType()
//                        + (artifact.getClassifier() == null ? "" : (":" + artifact.getClassifier()))
                );
            }
            return rc;
        }

        private boolean isExclusivelyType(String value) {
            Set<String> types = getTypes();
            return types.size() == 1 && types.contains(value);
        }

        public int getRecursiveChildCount() {
            int rc = children.size();
            for (Edge child : children) {
                int t = child.getRecursiveChildCount();
                if (t > rc) {
                    rc = t;
                }
            }
            return rc;
        }

    }

    private class Edge {
        private Node parent;
        private Node child;
        private String scope;
        private boolean optional;
        private DependencyNode dependencyNode;

        public Edge(Node parent, Node child, DependencyNode dependencyNode) {
            this.parent = parent;
            this.child = child;
            this.dependencyNode = dependencyNode;
            this.scope = dependencyNode.getArtifact().getScope();
            this.optional = dependencyNode.getArtifact().isOptional();
        }

        public Edge(Edge edge) {
            this.parent = edge.parent;
            this.child = edge.child;
            this.dependencyNode = edge.dependencyNode;
            this.scope = edge.scope;
            this.optional = edge.optional;
        }

        public Edge optional(boolean optional) {
            if (this.optional == optional) {
                return this;
            }
            Edge rc = new Edge(this);
            rc.optional = optional;
            return rc;
        }

        public Edge scope(String scope) {
            if (this.scope.equals(scope)) {
                return this;
            }
            Edge rc = new Edge(this);
            rc.scope = scope;
            return rc;
        }

        public boolean isHidden() {
            if (hideTransitive && dependencyNode.getParent().getParent() != null) {
                return true;
            }
            if (hideOptional && optional)
                return true;
            if (!scopes.contains(scope))
                return true;

            final int state = dependencyNode.getState();
            if (hideOmitted && (state == DependencyNode.OMITTED_FOR_CONFLICT || state == DependencyNode.OMITTED_FOR_CYCLE)) {
                return true;
            }
            return false;
        }

        public boolean isScope(String s) {
            return scope.equals(s);
        }

        public String getLineStyle() {
            if (optional) {
                return "dotted";
            }
            return "solid";
        }

        public String getLabel() {
            StringBuilder sb = new StringBuilder();
            if (!isScope("compile")) {
                sb.append(scope);
            }
            if (optional) {
                if (sb.length() != 0) {
                    sb.append(",");
                }
                sb.append("optional");
            }
            return sb.toString();
        }

        public String getColor() {
            if (isScope("test")) {
                return "#0c7500";
            }
            return "black";
        }

        double getWeight() {
            double rc = 1 + getRecursiveChildCount();
            if (isScope("compile")) {
                rc *= 2;
            }
            if (!optional) {
                rc *= 2;
            }
            return rc;
        }

        private int getRecursiveChildCount() {
            return child.getRecursiveChildCount();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            Edge edge = (Edge) o;

            if (parent != null ? !parent.equals(edge.parent) : edge.parent != null) return false;
            if (child != null ? !child.equals(edge.child) : edge.child != null) return false;
            if (scope != null ? !scope.equals(edge.scope) : edge.scope != null) return false;
            if (optional != edge.optional) return false;
            return true;
        }

        @Override
        public int hashCode() {
            int result = parent != null ? parent.hashCode() : 0;
            result = 31 * result + (child != null ? child.hashCode() : 0);
            result = 31 * result + (scope != null ? scope.hashCode() : 0);
            result = 31 * result + (optional ? 1 : 0);
            return result;
        }

        @Override
        public String toString() {
            return "Edge{" +
                    "parent=" + parent +
                    ", child=" + child +
                    ", scope='" + scope + '\'' +
                    ", optional=" + optional +
                    '}';
        }
    }

    public void add(DependencyNode dn) {
        add(dn, true);
    }

    private Node add(DependencyNode dn, boolean root) {
        Node parent = getNode(dn);
        if (root) {
            parent.roots++;
        }
        if (dn.hasChildren()) {
            for (DependencyNode c : (List<DependencyNode>) dn.getChildren()) {
                if (filter == null || filter.accept(c)) {
                    Node child = add(c, false);
                    Edge edge = new Edge(parent, child, c);
                    add(edge);
                }
            }
        }
        return parent;
    }

    private Node getNode(DependencyNode dn) {
        Artifact artifact = dn.getArtifact();
        String id = artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getBaseVersion();
//        if (artifact.getClassifier() != null) {
//            id += ":" + artifact.getClassifier();
//        }
        Node node = nodes.get(id);
        if (node == null) {
            node = new Node(id, dn.getArtifact());
            nodes.put(id, node);

            for (ClusterGraph entry : clustersMap) {
                String category = entry.getCategory();
                if (id.startsWith(category)) {
                    Cluster cluster = clusters.get(category);
                    if (cluster == null) {
                        cluster = new Cluster(category, entry.getName(), entry.getColor());
                        clusters.put(category, cluster);
                    }
                    cluster.getChildren().add(node);
                    break;
                }
            }

        }
        return node;
    }

    private void add(Edge edge) {
        if (edges.add(edge)) {
            edge.child.parents.add(edge);
            edge.parent.children.add(edge);
        }
    }

    private void remove(Node node) {
        nodes.remove(node.getId());

        for (Cluster cluster : clusters.values()) {
            cluster.getChildren().remove(node);
        }

        // Remove the edges attached to this node...
        for (Edge edge : new ArrayList<Edge>(node.parents)) {
            remove(edge);
        }
        for (Edge edge : new ArrayList<Edge>(node.children)) {
            remove(edge);
        }
    }

    private void remove(Edge edge) {
        edge.parent.children.remove(edge);
        edge.child.parents.remove(edge);
        edges.remove(edge);
    }

    public void export(File target) throws MojoExecutionException {

        for (Edge edge : new ArrayList<Edge>(edges)) {
            if (edge.isHidden()) {
                log.debug("Dropping hidden edge: " + edge);
                remove(edge);
            }

        }

        if (cascade) {
            // Propagate the attributes down to the children.

            LinkedList<Node> ll = new LinkedList<Node>(nodes.values());
            while (!ll.isEmpty()) {
                // Optional propagates...
                Node node = ll.removeFirst();
                if (node.isOptional()) {
                    for (Edge edge : new ArrayList<Edge>(node.children)) {
                        if (!edge.optional) {
                            remove(edge);
                            add(edge.optional(true));

                            // If a child filpped to optional.. then we need
                            // to enqueue so we process it's children
                            if (edge.child.isOptional()) {
                                ll.addLast(edge.child);
                            }
                        }
                    }
                }

                // scope propagates....
                if (node.isScope("test")) {
                    for (Edge edge : new ArrayList<Edge>(node.children)) {
                        if (!edge.isScope("test")) {
                            remove(edge);
                            add(edge.scope("test"));

                            // If a child filpped to test.. then we need
                            // to enqueue so we process it's children
                            if (edge.child.isScope("test")) {
                                ll.addLast(edge.child);
                            }
                        }
                    }
                }
            }
        }

        // Remove all the non root nodes that are disconnected.
        for (Node node : new ArrayList<Node>(nodes.values())) {
            if (node.parents.size() == 0 && node.roots == 0) {
                log.debug("Dropping orphaned node: " + node);
                remove(node);
            }
        }

        // Write the source file...
        boolean convertDotFile = true;
        File source = new File(target.getParentFile(), target.getName() + ".dot");

        // User might just be requesting a dot file..
        if (target.getName().endsWith(".dot")) {
            convertDotFile = false;
            source = target;
        }

        PrintStream os = null;
        try {
            log.debug("Exporting to: " + source);
            os = new PrintStream(source);
            DotExporter exporter = new DotExporter(os);
            exporter.export();
        } catch (Exception e) {
            throw new MojoExecutionException("Could not create the dot file used to generate the image.", e);
        } finally {
            os.close();
        }


        if (!convertDotFile) {
            return;
        }

        try {
            Commandline commandline = new Commandline();
            try {
                commandline.addSystemEnvironment();
            } catch (Exception ignore) {
            }
            commandline.setExecutable("dot");
            commandline.addArguments(new String[]{
                    "-T" + FileUtils.getExtension(target.getName()),
                    "-o" + target.getAbsolutePath(),
                    source.getAbsolutePath()
            });

            log.debug("Executing dot command...");
            int rc = CommandLineUtils.executeCommandLine(commandline, new DefaultConsumer(), new DefaultConsumer());
            if (rc != 0) {
                throw new MojoExecutionException("Execution of the 'dot' command failed.  Perhaps it's not installed.  See: http://www.graphviz.org/");
            }
            log.debug("Graph generated. ");
            if (!keepDot) {
                source.delete();
            }

        } catch (CommandLineException e) {
            throw new MojoExecutionException("Execution of the 'dot' command failed.", e);
        }

    }

    private class DotExporter {
        private final PrintStream out;
        int indent = 0;

        public DotExporter(PrintStream os) {
            this.out = os;
        }

        public void export() {

            String graphFont = "Serif";
            String nodeFont = "SanSerif";

            String osName = System.getProperty("os.name", "NO OS NAME!!");
            if (osName.contains("Windows")) {
                graphFont = "arial";
                nodeFont = "arial";
            }

            p("digraph dependencies {").i(1);
            {
                p("graph [").i(1);
                {
                    if (label != null) {
                        p("label=" + q(label));
                    }
                    p("labeljust=l");
                    p("labelloc=t");
                    p("fontsize=18");
                    p("fontname=" + q(graphFont));
                    p("ranksep=1");
                    p("rankdir=" + q(direction));
                    p("nodesep=.05");

                }
                i(-1).p("];");
                p("node [").i(1);
                {
                    p("fontsize=8");
                    p("fontname=" + q(nodeFont));
                    p("shape=\"rectangle\"");
                }
                i(-1).p("];");
                p("edge [").i(1);
                {
                    p("fontsize=8");
                    p("fontname=" + q(nodeFont));
                }
                i(-1).p("];");

                int clusterCounter = 1;
                for (Cluster cluster : clusters.values()) {
                    if (cluster.getChildren().size() > 0) {
                        p("subgraph cluster_" + clusterCounter++ + " {").i(1);
                        {
                            p("label=\"" + cluster.getLabel() + "\"");
                            p("color=\"" + cluster.getColor() + "\"");

                            // Write the nodes..
                            for (Node node : cluster.getChildren()) {
                                p(q(node.getId()) + " [").i(1);
                                {
                                    p("fontsize=" + node.getFontSize());
                                    p("label=" + q(node.getLabel()));
                                    p("color=" + q(node.getColor()));
                                    p("fontcolor=" + q(node.getFontColor()));
                                    p("fillcolor=" + q(node.getFillColor()));
                                    p("style=" + q(node.getLineStyle()));
                                }
                                i(-1).p("];");
                            }

                        }
                        i(-1).p("}");
                    }
                }

                // Write the nodes..
                for (Node node : nodes.values()) {
                    boolean isFree = true;
                    for (Cluster cluster : clusters.values()) {
                        if (cluster.getChildren().contains(node)) {
                            isFree = false;
                        }
                    }

                    if (isFree) {
                        p(q(node.getId()) + " [").i(1);
                        {
                            p("fontsize=" + node.getFontSize());
                            p("label=" + q(node.getLabel()));
                            p("color=" + q(node.getColor()));
                            p("fontcolor=" + q(node.getFontColor()));
                            p("fillcolor=" + q(node.getFillColor()));
                            p("style=" + q(node.getLineStyle()));
                        }
                        i(-1).p("];");
                    }
                }

                // Write the edges..
                for (Edge edge : edges) {
                    p(q(edge.parent.getId()) + " -> " + q(edge.child.getId()) + " [").i(1);
                    {
                        p("label=" + q(edge.getLabel()));
                        p("style=" + q(edge.getLineStyle()));
                        p("color=" + q(edge.getColor()));
                        p("fontcolor=" + q(edge.getColor()));
                        p("weight=" + edge.getWeight());
                    }
                    i(-1).p("];");
                }

            }
            i(-1).p("}");
        }

        private String q(String value) {
            return "\"" + value + "\"";
        }

        private DotExporter i(int indent) {
            this.indent += indent;
            return this;
        }

        private DotExporter p(String x) {
            for (int i = 0; i < indent; i++) {
                out.print("  ");
            }
            out.println(x);
            return this;
        }

    }


}
